
package com.code.ape.codeape.inhos.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.code.ape.codeape.admin.api.entity.SysUser;
import com.code.ape.codeape.admin.api.feign.RemoteUserService;
import com.code.ape.codeape.common.core.util.AssertUtil;
import com.code.ape.codeape.common.core.util.R;
import com.code.ape.codeape.common.core.util.RetOps;
import com.code.ape.codeape.common.elasticsearch.ResetElasticSearchClient;
import com.code.ape.codeape.common.security.service.CodeapeUser;
import com.code.ape.codeape.common.security.util.SecurityUtils;
import com.code.ape.codeape.inhos.api.dto.PatInfoDTO;
import com.code.ape.codeape.inhos.api.entity.*;
import com.code.ape.codeape.inhos.api.entity.constant.*;
import com.code.ape.codeape.inhos.api.util.RegexUtil;
import com.code.ape.codeape.inhos.api.vo.PatInfoVO;
import com.code.ape.codeape.inhos.mapper.*;
import com.code.ape.codeape.inhos.mq.callback.PatMsgSendCallback;
import com.code.ape.codeape.inhos.service.PatInfoHotService;
import com.code.ape.codeape.inhos.service.PatRecordService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.SetUtils;
import org.apache.lucene.search.TotalHits;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.core.CountResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 住院患者
 *
 * @author pig code generator
 * @date 2023-06-02 11:34:04
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class PatInfoHotServiceImpl extends ServiceImpl<PatInfoHotMapper, PatInfoHot> implements PatInfoHotService {

	private final PatRecordService patRecordService;

	private final PatRecordMapper patRecordMapper;

	private final PatTransferMapper patTransferMapper;

	private final RemoteUserService remoteUserService;

	private final RocketMQTemplate rocketMQTemplate;

	private final MsgRecordMapper msgRecordMapper;

	private final PatInfoColdMapper patInfoColdMapper;

	private final ResetElasticSearchClient resetElasticSearchClient;


	@Transactional
	@Override
	public Boolean savePat(PatInfoDTO dto) {
		CodeapeUser codeapeUser = Objects.requireNonNull(SecurityUtils.getUser());
		//step1 校验住院号，需要保持在院患者中的住院号唯一
		Long total = baseMapper.selectCount(Wrappers.<PatInfoHot>lambdaQuery()
				.eq(PatInfoHot::getHosId, codeapeUser.getHosId())
				.eq(PatInfoHot::getIptNum, dto.getIptNum())
				.eq(PatInfoHot::getStatus, PatInfoStatusEnum.IN_HOS.getValue()));
		AssertUtil.isTrue(total == 0, "住院号重复！");

		//step2 构建数据
		PatInfoHot patInfo = BeanUtil.toBean(dto, PatInfoHot.class);
		//住院流水号，唯一识别ID
		patInfo.setVisitId(IdWorker.getIdStr())
				//正则取出住院号
				.setBedSort(StrUtil.isNotBlank(dto.getBedNum()) ? RegexUtil.getNumber(dto.getBedNum(), PatInfoConstant.REGULAR_EXPRESSION) : dto.getBedNum())
				//第一次住院
				.setVisitTime(1L)
				.setStatus(PatInfoStatusEnum.IN_HOS.getValue())
				.setHosId(codeapeUser.getHosId());

		//step3 入库
		int patRows = baseMapper.insert(patInfo);
		AssertUtil.isTrue(patRows == 1, "添加患者失败！");

		//step4 添加入院记录
		PatRecord patRecord = PatRecord.builder()
				.bedNum(dto.getBedNum())
				.deptId(dto.getDeptId())
				.visitId(patInfo.getVisitId())
				.docId(dto.getDocId())
				.iptNum(dto.getIptNum())
				.iptTime(dto.getIptTime())
				.hosId(codeapeUser.getHosId())
				.nurseId(dto.getNurseId())
				.patId(patInfo.getId())
				.iptType(PatRecordTypeEnum.IN_HOS.getValue())
				.build();
		int b = patRecordMapper.insert(patRecord);
		AssertUtil.isTrue(b == 1, "添加患者失败！");
		return Boolean.TRUE;
	}

	/**
	 * 针对同一个患者的更改场景很少，无需考虑并发高的场景
	 */
	@Override
	public Boolean updatePat(PatInfoDTO dto) {
		//step1 查库
		PatInfoHot patInfo = baseMapper.selectById(dto.getId());
		AssertUtil.isTrue(Objects.nonNull(patInfo), "患者不存在！");

		//step2 如果住院号不为空，则要校验住院号的唯一性
		if (StrUtil.isNotBlank(dto.getIptNum()) && !StrUtil.equals(dto.getIptNum(), patInfo.getIptNum())) {
			Long total = baseMapper.selectCount(Wrappers.<PatInfoHot>lambdaQuery()
					.eq(PatInfoHot::getHosId, Objects.requireNonNull(SecurityUtils.getUser()).getHosId())
					.eq(PatInfoHot::getIptNum, dto.getIptNum())
					.eq(PatInfoHot::getStatus, PatInfoStatusEnum.IN_HOS.getValue()));
			AssertUtil.isTrue(total == 0, "住院号重复！");
		}

		//step3 如果科室改变了，则添加转科记录
		if (Objects.nonNull(dto.getDeptId()) && !dto.getDeptId().equals(patInfo.getDeptId())) {
			PatTransfer patTransfer = PatTransfer.builder()
					.patId(dto.getId())
					.visitId(patInfo.getVisitId())
					.bedNum(dto.getBedNum())
					.iptNum(dto.getIptNum())
					.hosId(Objects.requireNonNull(SecurityUtils.getUser()).getHosId())
					.docId(dto.getDocId())
					.newDept(dto.getDeptId())
					.oldDept(patInfo.getDeptId())
					.nurseId(dto.getNurseId())
					.transferTime(LocalDateTime.now())
					.build();
			int transferRows = patTransferMapper.insert(patTransfer);
			AssertUtil.isTrue(transferRows == 1, "更新失败！");
		}

		//step4 更新
		PatInfoHot patInfoHot = BeanUtil.toBean(dto, PatInfoHot.class);
		int patRows = baseMapper.updateById(patInfoHot);
		AssertUtil.isTrue(patRows == 1, "更新失败！");
		return Boolean.TRUE;
	}

	@Override
	public Page<PatInfoVO> listPage(Page<PatInfoVO> page, PatInfoDTO dto) {
		Page<PatInfoVO> pageVo = baseMapper.selectPatInfoPage(page, dto);
		return pageVo;
	}

	@SneakyThrows
	@Override
	public Page<PatInfoVO> listOutPage(Page<PatInfoVO> page, PatInfoDTO dto) {
		//此处直接从ElasticSearch中查询，todo 实际生产中如果是独立部署，可以直接从库中查询
		SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
		//设置根据出院时间倒序排列
		sourceBuilder.sort(new FieldSortBuilder("leaveTime").order(SortOrder.DESC));
		//构建一个bool查询
		BoolQueryBuilder builder = QueryBuilders.boolQuery();

		//姓名查询
		if (StrUtil.isNotBlank(dto.getName())) {
			builder.must(QueryBuilders.matchQuery("name", dto.getName()));
		}

		//住院号
		if (StrUtil.isNotBlank(dto.getIptNum())) {
			builder.must(QueryBuilders.termQuery("iptNum", dto.getIptNum()));
		}

		//科室Id
		if (Objects.nonNull(dto.getDeptId())) {
			builder.must(QueryBuilders.termQuery("deptId", dto.getDeptId()));
		}

		//查询总数
		CountResponse countResponse = resetElasticSearchClient.getTotal(PatInfoConstant.PAT_INFO_INDEX_NAME, new SearchSourceBuilder(), builder);
		AssertUtil.isTrue(countResponse.status().equals(RestStatus.OK), "查询失败！");
		long total = countResponse.getCount();
		if (total == 0) {
			return page;
		}

		//分页查询数据
		SearchResponse searchResponse = resetElasticSearchClient.searchDataPage(PatInfoConstant.PAT_INFO_INDEX_NAME, page.getCurrent(), page.getSize(), sourceBuilder, builder);
		AssertUtil.isTrue(searchResponse.status().equals(RestStatus.OK), "查询失败！");
		List<PatInfoVO> list = new ArrayList<>();
		//命中的数量
		long totalHits = searchResponse.getHits().getTotalHits().value;
		SearchHit[] searchHits = searchResponse.getHits().getHits();
		for (SearchHit searchHit : searchHits) {
			String json = searchHit.getSourceAsString();
			PatInfoVO patInfoVO = JSON.parseObject(json, PatInfoVO.class);
			list.add(patInfoVO);
		}
		page.setRecords(list).setTotal(total).setPages(total%page.getSize()==0?total/page.getSize():total/page.getSize()+1);
		return page;
	}

	@Transactional
	@Override
	public Boolean leavePat(PatInfoDTO dto) {
		//step1 查库
		PatInfoHot patInfoHot = baseMapper.selectById(dto.getId());
		AssertUtil.isTrue(Objects.nonNull(patInfoHot), "该患者不存在！");

		//step2 直接删除热表中数据
		int patRows = baseMapper.deleteNoLogic(dto.getId());
		AssertUtil.isTrue(patRows == 1, "出院失败！");

		//step3 入院记录设置出院时间
		PatRecord patRecord = patRecordService.getOne(Wrappers.<PatRecord>lambdaQuery()
				.eq(PatRecord::getPatId, patInfoHot.getId())
				.eq(PatRecord::getIptNum, patInfoHot.getIptNum())
				.eq(PatRecord::getIptTime, patInfoHot.getIptTime())
				.eq(PatRecord::getIptType, PatRecordTypeEnum.IN_HOS.getValue()));
		AssertUtil.isTrue(Objects.nonNull(patRecord), "缺少入院记录！");
		PatRecord record = PatRecord.builder()
				.id(patRecord.getId())
				.leaveTime(Optional.ofNullable(dto.getLeaveTime()).orElse(LocalDateTime.now()))
				.iptType(PatRecordTypeEnum.OUT_HOS.getValue())
				.build();
		boolean b = patRecordService.updateById(record);
		AssertUtil.isTrue(b, "出院失败！");

		//step4 迁移至冷库中
		PatInfoCold patInfoCold = BeanUtil.toBean(patInfoHot, PatInfoCold.class);
		patInfoCold.setLeaveTime(record.getLeaveTime());
		patInfoCold.setStatus(PatInfoStatusEnum.OUT_HOS.getValue());
		int patColdRows = patInfoColdMapper.insert(patInfoCold);
		AssertUtil.isTrue(patColdRows==1,"出院失败！");

		//todo step5 调用医嘱服务关闭该患者的全部医嘱

		//todo step6 调用血糖服务获取出院的血糖数据

		//step7 发送一条MQ消息，触发冷热分离
		PatInfoVO patInfoVO = BeanUtil.toBean(patInfoCold, PatInfoVO.class);
		String msgText = JSON.toJSONString(patInfoVO);
		String topicName=MessageFormat.format("{0}:{1}",MqConstant.PAT_TOPIC,MqConstant.TAG_COLD);
		//生成一条消息日志，保证可靠性投递
		MsgRecord msgRecord = MsgRecord.builder()
				.topic(topicName)
				.content(msgText)
				//设置初始状态：已发送
				.status(MsgStatusEnum.MSG_SEND.getValue())
				.build();
		//插入消息日志
		int msgRows = msgRecordMapper.insert(msgRecord);
		AssertUtil.isTrue(msgRows == 1, "消息日志记录失败！");

		try {
			//这里try起来，已经做了消息日志，即使失败了定时任务也会定时推送，不能影响主业务
			//异步消息发送，有回调接口，不用阻塞主业务
			rocketMQTemplate.asyncSend(topicName, msgText, new PatMsgSendCallback(msgRecord.getId()));
		} catch (Exception ex) {
			log.error("患者出院冷热分离的消息发送异常：{}", ex.getLocalizedMessage());
		}
		return Boolean.TRUE;
	}

	@Transactional
	@Override
	public Boolean inPat(PatInfoDTO dto) {
		//step1 查库
		PatInfoCold coldPat = patInfoColdMapper.selectById(dto.getId());
		AssertUtil.isTrue(Objects.nonNull(coldPat),"患者不存在！");

		CodeapeUser codeapeUser = Objects.requireNonNull(SecurityUtils.getUser());
		//step2 校验住院号，需要保持在院患者中的住院号唯一
		Long total = baseMapper.selectCount(Wrappers.<PatInfoHot>lambdaQuery()
				.eq(PatInfoHot::getHosId, codeapeUser.getHosId())
				.eq(PatInfoHot::getIptNum, dto.getIptNum())
				.eq(PatInfoHot::getStatus, PatInfoStatusEnum.IN_HOS.getValue()));
		AssertUtil.isTrue(total == 0, "住院号重复！");

		//step3 构建数据
		PatInfoHot patInfo = BeanUtil.toBean(dto, PatInfoHot.class);
		//住院流水号，唯一识别ID，患者一生的住院记录中保持一致
		patInfo.setVisitId(coldPat.getVisitId())
				//正则取出住院号
				.setBedSort(StrUtil.isNotBlank(dto.getBedNum()) ? RegexUtil.getNumber(dto.getBedNum(), PatInfoConstant.REGULAR_EXPRESSION) : dto.getBedNum())
				//住院次数+1
				.setVisitTime(coldPat.getVisitTime()+1)
				//住院状态
				.setStatus(PatInfoStatusEnum.IN_HOS.getValue())
				.setHosId(codeapeUser.getHosId());

		//step4 入库
		int patRows = baseMapper.insert(patInfo);
		AssertUtil.isTrue(patRows == 1, "办理住院失败！");

		//step4 添加入院记录
		PatRecord patRecord = PatRecord.builder()
				.bedNum(dto.getBedNum())
				.deptId(dto.getDeptId())
				.visitId(patInfo.getVisitId())
				.docId(dto.getDocId())
				.iptNum(dto.getIptNum())
				.iptTime(dto.getIptTime())
				.hosId(codeapeUser.getHosId())
				.nurseId(dto.getNurseId())
				.patId(patInfo.getId())
				.iptType(PatRecordTypeEnum.IN_HOS.getValue())
				.build();
		int b = patRecordMapper.insert(patRecord);
		AssertUtil.isTrue(b == 1, "办理住院失败！");

		//step5 删除冷裱中的数据
		int patColdRows = patInfoColdMapper.deleteNoLogic(dto.getId());
		AssertUtil.isTrue(patColdRows==1,"办理住院失败");

		//step6 发送消息删除冷库中的数据、ElasticSearch中的数据
		//生成一条消息日志，保证可靠性投递
		String msgText=dto.getId().toString();
		String topicName=MessageFormat.format("{0}:{1}",MqConstant.PAT_TOPIC,MqConstant.TAG_HOT);
		MsgRecord msgRecord = MsgRecord.builder()
				.topic(topicName)
				.content(msgText)
				//设置初始状态：已发送
				.status(MsgStatusEnum.MSG_SEND.getValue())
				.build();
		//插入消息日志
		int msgRows = msgRecordMapper.insert(msgRecord);
		AssertUtil.isTrue(msgRows == 1, "消息日志记录失败！");

		try {
			//这里try起来，已经做了消息日志，即使失败了定时任务也会定时推送，不能影响主业务
			//异步消息发送，有回调接口，不用阻塞主业务
			rocketMQTemplate.asyncSend(topicName, msgText, new PatMsgSendCallback(msgRecord.getId()));
		} catch (Exception ex) {
			log.error("患者办理入院冷热分离的消息发送异常：{}", ex.getLocalizedMessage());
		}
		return Boolean.TRUE;
	}

	@Override
	public PatInfoVO getPatById(Long id) {
		//step1 查询
		PatInfoHot patInfoHot = baseMapper.selectById(id);
		PatInfoVO patInfoVO = BeanUtil.toBean(patInfoHot, PatInfoVO.class);

		//step2 获取医生、护士姓名
		Set<Long> enterIds = SetUtils.hashSet(patInfoVO.getDocId(), patInfoVO.getNurseId()).stream().filter(Objects::nonNull).collect(Collectors.toSet());
		//step3 调用admin服务获取，入库人姓名不是展示必须，即使服务调用失败也不抛出异常，try起来
		if (CollectionUtil.isNotEmpty(enterIds)) {
			List<SysUser> enterList = new ArrayList<>();
			try {
				R<List<SysUser>> resultEnterUser = remoteUserService.listUserInfoByIds(enterIds);
				enterList = RetOps.of(resultEnterUser).getData().orElse(new ArrayList<>());
			} catch (Exception ex) {
				log.error("调用用户服务异常，参数：{}，异常信息：{}", JSON.toJSON(enterIds), ex.getLocalizedMessage());
			}

			if (CollectionUtil.isNotEmpty(enterList)) {
				Map<Long, SysUser> userMap = enterList.stream().collect(Collectors.groupingBy(SysUser::getUserId, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
				patInfoVO.setDocName(Objects.nonNull(userMap.get(patInfoVO.getDocId())) ? userMap.get(patInfoVO.getDocId()).getName() : null);
				patInfoVO.setNurseName(Objects.nonNull(userMap.get(patInfoVO.getNurseId())) ? userMap.get(patInfoVO.getNurseId()).getName() : null);
			}
		}
		return patInfoVO;
	}

}
